use std::collections::*;
use std::convert::From;
use std::sync::Arc;
use std::time;

use craftdig_clientlib::player::ControlInput;
use craftdig_clientlib::{client::ClientHandle, SceneController};
use craftdig_core::registrar;
use craftdig_datamodel::block::action::ActionData;
use craftdig_datamodel::block::model::{Collider, Solidity};
use craftdig_datamodel::block::BlkId;
use craftdig_datamodel::chunk::ChunkBitmap;
use craftdig_datamodel::entity::EntityId;
use craftdig_datamodel::world::{editable, WorldView};
use craftdig_datamodel::{block, world};
use craftdig_math::*;

use godot::classes::input::MouseMode;
use godot::classes::{
    CharacterBody3D, CollisionObject3D, Engine, GeometryInstance3D, Input, InputEvent, Label,
    Material, Mesh, MeshInstance3D, Node, PackedScene, PhysicsRayQueryParameters3D, RayCast3D,
};
use godot::prelude::*;

use crate::blocktab;
use crate::chunk_builder::ChunkBuilder;
use crate::containers::{HudView, PlayerView, WorldSceneView};
use crate::input::InputState;

#[derive(GodotClass)]
#[class(base = Node)]
pub struct CraftdigBridge {
    // Parameters.
    #[export]
    reach_length: f32,

    #[export]
    selected_blkid: u16,

    /// Limit of chunks we can re-render per frame.
    #[export]
    chunk_update_rate: u16,

    #[var(hint = MULTILINE_TEXT, usage_flags = [EDITOR, STORAGE, SCRIPT_VARIABLE])]
    init_world_script: GString,

    // World objects
    /// Marker template we instantiate on interactions.
    marker_tmplt: Gd<PackedScene>,

    /// Player reference.
    #[export]
    player: Option<Gd<CharacterBody3D>>,

    /// Camera reference.
    #[export]
    camera: Option<Gd<Node3D>>,

    /// Selection reference.
    #[export]
    selection: Option<Gd<Node3D>>,

    /// Chunk block material
    #[export]
    block_mat: Option<Gd<Material>>,

    #[export]
    lbl_coords: Option<Gd<Label>>,

    #[export]
    lbl_blkty: Option<Gd<Label>>,

    /// Inner run-time bridge implementation.
    bridge_impl: Option<BridgeImpl>,

    // Meta
    #[base]
    node: Base<Node>,
}

#[godot_api]
impl CraftdigBridge {
    // stub
}

#[godot_api]
impl INode for CraftdigBridge {
    fn init(node: Base<Node>) -> Self {
        godot_print!("craftdig bridge constructed");

        let test_script = craftdig_dataparser::world_inst::TEST_SCRIPT;

        Self {
            selected_blkid: 1,
            chunk_update_rate: 4,
            init_world_script: GString::from(test_script),
            marker_tmplt: PackedScene::new_gd(),
            player: None,
            camera: None,
            selection: None,
            reach_length: 4.0,
            block_mat: None,
            lbl_coords: None,
            lbl_blkty: None,
            bridge_impl: None,
            node,
        }
    }

    fn ready(&mut self) {
        // Check if we're in the editor, if so, exit.
        if Engine::singleton().is_editor_hint() {
            return;
        }

        self.marker_tmplt = match try_load::<PackedScene>("res://prefabs/marker.tscn") {
            Ok(tmplt) => tmplt,
            Err(e) => {
                godot_error!("bridge: unable to load marker prefab (aborting rest of init): {e}");
                return;
            }
        };

        let chunks_root = if let Some(node) = self.to_gd().find_child("ChunksRoot".into()) {
            if let Ok(node3d) = node.try_cast::<Node3D>() {
                node3d
            } else {
                godot_error!("bridge: chunks root not Node3D, aborting rest of init");
                return;
            }
        } else {
            godot_error!("bridge: unable to find chunks root, aborting rest of init");
            return;
        };

        // Assign all the state view and other utility types.
        let hud_view = HudView::new(
            self.lbl_coords
                .as_ref()
                .expect("bridge: missing coords label")
                .clone(),
            self.lbl_blkty
                .as_ref()
                .expect("bridge: missing blkty label")
                .clone(),
        );

        let player_view = PlayerView::new(
            self.player
                .as_ref()
                .expect("bridge: missing player node")
                .clone(),
            self.camera
                .as_ref()
                .expect("bridge: missing camera node")
                .clone(),
            self.selection
                .as_ref()
                .expect("bridge: missing selection node")
                .clone(),
        );

        let mut world_view = WorldSceneView::new(chunks_root);

        // Now instantiate a chunk.
        /*let inst = craftdig_dataparser::world_inst::make_base_instantiator();

        let world_script = self.init_world_script.to_string();
        let Ok(chunks) = inst.render(&world_script) else {
            godot_error!("bridge: failed to init world instantiator, aborting rest of init");
            return;
        };*/

        // Start the client.
        let block_reg = blocktab::make_block_ty_registry();
        let msg_reg = blocktab::make_msg_ty_registry();
        let reg = Arc::new(registrar::GameRegistrar::new(block_reg, msg_reg));
        let ch = craftdig_clientlib::client::launch(reg);
        ch.on_init();

        // Set the chunk builder.
        let block_mat = self
            .block_mat
            .as_ref()
            .expect("bridge: missing block material")
            .clone();
        let chunk_builder = ChunkBuilder::new(ch.registrar().clone(), block_mat);

        // Manually link up the player node in the world scene.
        let player_node = self
            .player
            .as_ref()
            .expect("bridge: missing player node")
            .clone()
            .upcast::<Node3D>();
        world_view.set_entity_node_manually(*ch.player_id().as_entity_id(), player_node);

        let bridge_impl = BridgeImpl {
            client_handle: ch,
            chunk_builder,
            chunks_pending_refresh: VecDeque::new(),
            control_state: PlayerControlState {
                reach_len: self.reach_length,
                selected_blkid: self.selected_blkid,
            },
            view_config: ViewConfig {
                chunk_update_rate: self.chunk_update_rate,
            },
            hud_view,
            player_view,
            world_view,
        };

        self.bridge_impl = Some(bridge_impl);

        godot_print!("bridge: ready");
    }

    fn process(&mut self, delta: f64) {
        if let Some(bimpl) = self.bridge_impl.as_mut() {
            bimpl.on_frame(delta);
        } else {
            godot_error!("bridge: bridge uninitialized");
        }
    }
}

/// Data involved in managing how we process input from the player.
struct PlayerControlState {
    reach_len: f32,
    selected_blkid: u16,
}

impl PlayerControlState {
    pub fn get_selected_blkid(&self) -> BlkId {
        BlkId::from(self.selected_blkid)
    }
}

struct ViewConfig {
    /// Maximum number of chunks to update per frame.
    chunk_update_rate: u16,
}

/// Inner bridge implementation type that's constructed on `_ready`.
///
/// This avoids having to unwrap a bunch of options.
struct BridgeImpl {
    /// Inner generic game client handle.
    client_handle: ClientHandle,

    /// Container for building chunks.
    chunk_builder: ChunkBuilder,

    /// Chunks that we want to update but haven't yet.
    chunks_pending_refresh: VecDeque<ChunkCoord>,

    /// Player control state.
    control_state: PlayerControlState,

    /// Configuration for rendering.
    view_config: ViewConfig,

    // Various views.
    hud_view: HudView,
    player_view: PlayerView,
    world_view: WorldSceneView,
}

impl BridgeImpl {
    fn on_frame(&mut self, _delta: f64) {
        let inp = Input::singleton();
        let input_state = InputState::from_input(&inp);

        // Prepare a list of actions we're going to send off to the client to
        // interact with the world.
        let mut prepped_actions = Vec::new();

        // Check to see if the player is selecting a block.
        let facing_raw = self.player_view.get_player_facing();
        let facing = Vec3f::new(facing_raw.x, facing_raw.y, facing_raw.z).normalized();
        let camera_pos_raw = self.player_view.get_player_eye_pos();
        let camera_pos = Vec3f::new(camera_pos_raw.x, camera_pos_raw.y, camera_pos_raw.z);
        let raycast_target = camera_pos + facing * self.control_state.reach_len;
        let hit_data = self
            .client_handle
            .raycast_in_world(camera_pos, raycast_target);

        // If we hit something, try to figure out the block position.
        let sel_blk_pos = if let Some(hit) = hit_data {
            // Look up the selection AABB of the block.
            if let Some(blk_pos) = hit.block_pos() {
                let world = self.client_handle.cur_world_state();
                let id = world.get_block_id(blk_pos).unwrap_or(BlkId::zero());
                let aabb = self
                    .client_handle
                    .registrar()
                    .get_block_tydata_by_id(id)
                    .map(|td| *td.model().selection())
                    .unwrap_or_else(AABB::unit);

                // Set the selection box to the block.
                let sel_world_pos = Vector3::new(
                    blk_pos.x() as f32 + aabb.base().x(),
                    blk_pos.y() as f32 + aabb.base().y(),
                    blk_pos.z() as f32 + aabb.base().z(),
                );

                // Also set the scale so the selection matches the shape of the block.
                let sel_scale = Vector3::new(aabb.dims().x(), aabb.dims().y(), aabb.dims().z());

                self.player_view.set_selection(sel_world_pos, sel_scale);
            } else {
                // TODO something else?
            }

            hit.block_pos().map(|p| *p.inner())
        } else {
            self.player_view.clear_selection();
            None
        };

        // Check if attacking.
        if input_state.attacked {
            godot_print!("bridge: player attacked pos={sel_blk_pos:?} facing={facing:?} cam_pos={camera_pos:?}");

            // If a block is selected, submit the break action.
            if let Some(blk_pos) = sel_blk_pos {
                prepped_actions.push(ActionData::BreakBlock {
                    pos: BlkCoord::from(blk_pos),
                });
            }
        }

        // Check if placing a block.
        if input_state.interacted {
            godot_print!("bridge: player interacted pos={sel_blk_pos:?}");

            if let Some(blk_pos) = sel_blk_pos {
                let place_dir = hit_data.unwrap().hit_face_dir().to_unit_vec();
                let place_pos = blk_pos + place_dir;

                godot_print!("bridge: trying to place block at {place_pos}");

                let state =
                    block::BlockStateSnapshot::new_simple(self.control_state.get_selected_blkid());

                prepped_actions.push(ActionData::PlaceBlock {
                    pos: BlkCoord::from(place_pos),
                    new_state: state,
                });
            }
        }

        // Check if we should cycle the selected block.
        if input_state.cycle_block_off != 0 {
            let block_reg = self.client_handle.registrar().block_ty_reg();

            // Weird loop to pick the next block safely.
            let mut next_id =
                (self.control_state.selected_blkid as i16 + input_state.cycle_block_off) as u16;
            loop {
                match block_reg.get_by_id(BlkId::from(next_id)) {
                    Some(reg) => {
                        if reg.tydata().solidity() != Solidity::Empty {
                            break;
                        }
                    }
                    None => {
                        next_id = 1;
                        continue;
                    }
                }

                next_id += 1;
            }

            self.control_state.selected_blkid = next_id;
        }

        // Also check for keyboard movement inputs.
        {
            let mut controls = ControlInput::default();

            // If we haven't captured the mouse then we should act like we're
            // hands off the keyboard and mouse, so submit the default.
            if inp.get_mouse_mode() != MouseMode::CAPTURED {
                self.client_handle.update_control_input(controls);
                return;
            }

            // Get the inputs from the engine.
            let raw_move_vec = input_state.movement;

            // If there's no inputs them the normalization would turn into a
            // NaN, so only try to set a position when we do something.
            if raw_move_vec.magnitude() > 0.01 {
                // Take the inputs and adjust for the direction we're facing, which
                // for now is still managed on the Godot side.
                let gtrans = self.player_view.get_player_global_transform();
                let ixz = raw_move_vec.x() * gtrans.basis.col_a()
                    + raw_move_vec.y() * gtrans.basis.col_c();
                let move_vec = Vec3f::new_xz(ixz.x, ixz.z).normalized();
                controls.set_movement(move_vec);
            }

            controls.set_jumped(input_state.jumped);

            // Update the control inputs and apply things.
            self.client_handle.update_control_input(controls);
        }

        // Now that we've figured out the user actions, we can submit it to the
        // client.
        for act in prepped_actions {
            self.client_handle.submit_player_action(act);
        }

        let mut scene_ctrl = GodotSceneCtrl::new();
        if let Err(e) = self.client_handle.on_frame(&mut scene_ctrl) {
            godot_error!("craftdig: [RESYNC] {e}");
        }

        // Also update the positions of entities.
        for (eid, pos) in scene_ctrl.new_entity_poses_iter() {
            self.world_view.set_entity_position(eid, pos);
        }

        // Do some prep to figure out what the world looks like now.
        let cur_world = self.client_handle.cur_world_state();
        let world_diff = self.client_handle.pending_world_diff();
        let dw = editable::DiffedWorld::new(cur_world.as_ref(), world_diff);

        // Handle the chunks we were told to refresh.
        let ts_rerender_start = time::Instant::now();
        let player_chunk = self.player_view.get_player_chunk();
        let mut refresh_cnt = 0;
        for cc in scene_ctrl.dirty_chunks_iter() {
            // If we were told to refresh the player's chunk, do that
            // immediately and unconditionally.
            if cc == player_chunk {
                update_chunk_node(cc, &dw, &mut self.world_view, &self.chunk_builder);
                refresh_cnt += 1;
                continue;
            }

            if !self.chunks_pending_refresh.contains(&cc) {
                self.chunks_pending_refresh.push_back(cc);
            }
        }

        // Take some chunks off the pending queue and refresh them.
        while refresh_cnt < self.view_config.chunk_update_rate {
            let Some(cc) = self.chunks_pending_refresh.pop_front() else {
                break;
            };

            if dw.get_chunk_ref(cc).is_some() {
                update_chunk_node(cc, &dw, &mut self.world_view, &self.chunk_builder);
                refresh_cnt += 1;
            }
        }

        if refresh_cnt > 0 {
            let dur = time::Instant::now() - ts_rerender_start;
            let queued = self.chunks_pending_refresh.len();
            eprintln!("bridge: refreshed {refresh_cnt} chunks (took {dur:?}, queued {queued})");
        }

        // Set the player position label.
        let player_wp = self.player_view.get_player_world_pos();
        self.hud_view.set_player_coords_lbl(player_wp);

        // Set the block type label.
        let reg_name = self
            .client_handle
            .registrar()
            .get_block_tydata_by_id(self.control_state.get_selected_blkid())
            .map(|r| r.debug_name())
            .unwrap_or("[unknown]");
        self.hud_view.set_block_lbl(reg_name);
    }
}

struct GodotSceneCtrl {
    dirty_chunks: BTreeSet<ChunkCoord>,
    new_entity_poses: HashMap<EntityId, Vec3f>,
}

impl GodotSceneCtrl {
    pub fn new() -> Self {
        Self {
            dirty_chunks: BTreeSet::new(),
            new_entity_poses: HashMap::new(),
        }
    }

    pub fn dirty_chunks_iter(&self) -> impl Iterator<Item = ChunkCoord> + '_ {
        self.dirty_chunks.iter().copied()
    }

    pub fn new_entity_poses_iter(&self) -> impl Iterator<Item = (&EntityId, Vec3f)> {
        self.new_entity_poses.iter().map(|(e, p)| (e, *p))
    }
}

impl SceneController for GodotSceneCtrl {
    fn flag_dirty_chunk(&mut self, cc: ChunkCoord) {
        self.dirty_chunks.insert(cc);
    }

    fn show_chat_message(&mut self, msg: String) {
        // TODO make this show on-screen
        godot_print!("craftdig: [CHAT] {msg}");
    }

    fn set_entity_position(&mut self, entity: EntityId, pos: Vec3f) {
        self.new_entity_poses.insert(entity, pos);
    }
}

/// Creates or replaces a chunk container node in the world with new chunk state.
// TODO try to move this into `BridgeImpl`
pub fn update_chunk_node(
    coord: ChunkCoord,
    world: &impl WorldView,
    wview: &mut WorldSceneView,
    builder: &ChunkBuilder,
) {
    // If there's already a node for the chunk, remove it.
    wview.try_remove_chunk(coord);

    // Actually construct it.
    let new_chunk_cont = match builder.build_chunk_cont(coord, world) {
        Ok(cont) => cont,
        Err(e) => {
            godot_error!("bridge: failed to build chunk: {e}");
            return;
        }
    };

    // Insert the chunk node.
    wview.insert_chunk(coord, new_chunk_cont);

    godot_print!("bridge: updated chunk {coord:?}");
}
